-
Notifications
You must be signed in to change notification settings - Fork 10
Add rate limit to Port client #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
99b4284
to
01d6aa3
Compare
|
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/15832472715/artifacts/3386161105Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/15832678263/artifacts/3386278375Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are 2 things I don't like about this PR:
- You are implementing a rate limit yourself which seems an unnecessary complication. I'm sure there are great libraries for something this important.
- The rate limit is set globally. Shouldn't it only affect retries on the same request?
2bc681c
to
889d9e2
Compare
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/15846001956/artifacts/3390640592Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/15846038502/artifacts/3390655240Code Coverage Total Percentage:
|
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/15965961435/artifacts/3429784483Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
875daef
to
903a17d
Compare
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16095891109/artifacts/3471447766Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16096719592/artifacts/3471644206Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16096719592/artifacts/3471797920Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16097790940/artifacts/3471874623Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
2ec5266
to
bd1e329
Compare
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16109839014/artifacts/3474717624Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
internal/cli/client.go
Outdated
// WithRateLimitThreshold sets the threshold for when to start throttling | ||
// threshold should be between 0.0 and 1.0 (e.g., 0.1 means start throttling when 10% of requests remain) | ||
func WithRateLimitThreshold(threshold float64) Option { | ||
return func(pc *PortClient) { | ||
if threshold >= 0.0 && threshold <= 1.0 { | ||
pc.rateLimitManager.SetThreshold(threshold) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is too obscure. I think that you should return an error or panic if it is out of bounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option is to clamp
the threshold between 0
and 1
.
internal/ratelimit/ratelimit.go
Outdated
m.mu.Lock() | ||
m.activeRequests-- | ||
if m.activeRequests < 0 { | ||
m.activeRequests = 0 // Prevent negative count |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would log stuff like this as it shows we have a bug in our code
bd1e329
to
51cf48e
Compare
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16249103961/artifacts/3521483633Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Co-authored-by: Eyal Halpern Shalev <[email protected]>
Co-authored-by: Eyal Halpern Shalev <[email protected]>
bb27428
to
3d0ce6e
Compare
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16266967554/artifacts/3526735039Code Coverage Total Percentage:
|
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16266979758/artifacts/3526813956Code Coverage Total Percentage:
|
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16267878717/artifacts/3526902115Code Coverage Total Percentage:
|
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16268452431/artifacts/3527252617Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
Code Coverage Artifact 📈: https://github.com/port-labs/terraform-provider-port-labs/actions/runs/16268263599/artifacts/3527308241Code Coverage Total Percentage:
|
🚨 The new code coverage percentage is lower than the current one. Current coverage: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Use recover to catch any potential panics from semaphore operations | ||
if r := recover(); r != nil { | ||
m.logger.Debug("Recovered from panic in ResponseMiddleware defer", "panic", r) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this to work you need to wrap it inside a new defer func() {...}
// Use recover to catch any potential panics from semaphore operations | |
if r := recover(); r != nil { | |
m.logger.Debug("Recovered from panic in ResponseMiddleware defer", "panic", r) | |
} | |
defer func() { | |
// Use recover to catch any potential panics from semaphore operations | |
if r := recover(); r != nil { | |
m.logger.Debug("Recovered from panic in ResponseMiddleware defer", "panic", r) | |
} | |
} |
// Try to acquire semaphore immediately (non-blocking) as load indicator | ||
ctx, cancel := context.WithTimeout(context.Background(), 0) | ||
defer cancel() | ||
|
||
semaphoreAcquired := false | ||
if err := m.requestSemaphore.Acquire(ctx, 1); err != nil { | ||
m.logger.Warn("High concurrent load detected - proceeding anyway") | ||
} else { | ||
m.logger.Debug("Normal load - acquired semaphore slot") | ||
semaphoreAcquired = true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you wanted to have a timeout on the Acquire
, if you don't you can just use TryAcquire
instead.
// Try to acquire semaphore immediately (non-blocking) as load indicator | |
ctx, cancel := context.WithTimeout(context.Background(), 0) | |
defer cancel() | |
semaphoreAcquired := false | |
if err := m.requestSemaphore.Acquire(ctx, 1); err != nil { | |
m.logger.Warn("High concurrent load detected - proceeding anyway") | |
} else { | |
m.logger.Debug("Normal load - acquired semaphore slot") | |
semaphoreAcquired = true | |
} | |
semaphoreAcquired := m.requestSemaphore.TryAcquire(1) | |
if semaphoreAcquired { | |
m.logger.Debug("Normal load - acquired semaphore slot") | |
} else { | |
m.logger.Warn("High concurrent load detected - proceeding anyway") | |
} |
activeRequests := m.activeRequests.Add(-1) | ||
if activeRequests < 0 { | ||
// Reset to 0 if somehow went negative | ||
m.activeRequests.Store(0) | ||
activeRequests = 0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't atomic as well.
I see you choose to use a mutex, so just use it everywhere you do a check-and-update for m.activeRequests
and change m.activeRequests
to be a normal int
.
// contextKey is a custom type for context keys to avoid collisions | ||
type contextKey string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this comment is needed.
// contextKey is a custom type for context keys to avoid collisions | |
type contextKey string | |
type contextKey string |
assert.Nil(t, rateLimitInfo) | ||
} | ||
|
||
// Test-specific option functions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove comment. This is a test file so of course it is test specific.
// Test-specific option functions |
func TestClientRateLimitDisabled(t *testing.T) { | ||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("x-ratelimit-limit", "1000") | ||
w.Header().Set("x-ratelimit-remaining", "1") | ||
w.WriteHeader(http.StatusOK) | ||
_, _ = w.Write([]byte(`{"ok": true}`)) | ||
})) | ||
defer server.Close() | ||
|
||
client, err := New(server.URL, WithRateLimitDisabled()) | ||
require.NoError(t, err) | ||
|
||
resp, err := client.Client.R().Get("/test") | ||
require.NoError(t, err) | ||
assert.Equal(t, http.StatusOK, resp.StatusCode()) | ||
|
||
rateLimitInfo := client.RateLimitManager.GetInfo() | ||
assert.Nil(t, rateLimitInfo) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already tested in ratelimit
package. No need to double test things.
func TestClientRateLimitThrottling(t *testing.T) { | ||
skipIfDisabled(t) | ||
|
||
requestCount := 0 | ||
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
requestCount++ | ||
w.Header().Set("x-ratelimit-limit", "100") | ||
w.Header().Set("x-ratelimit-remaining", "5") | ||
w.Header().Set("x-ratelimit-reset", "2") | ||
w.WriteHeader(http.StatusOK) | ||
_, _ = w.Write([]byte(`{"ok": true}`)) | ||
})) | ||
defer server.Close() | ||
|
||
client, err := New(server.URL, WithRateLimitThreshold(0.1)) | ||
require.NoError(t, err) | ||
|
||
start := time.Now() | ||
resp, err := client.Client.R().Get("/test1") | ||
require.NoError(t, err) | ||
assert.Equal(t, http.StatusOK, resp.StatusCode()) | ||
|
||
resp, err = client.Client.R().Get("/test2") | ||
elapsed := time.Since(start) | ||
|
||
require.NoError(t, err) | ||
assert.Equal(t, http.StatusOK, resp.StatusCode()) | ||
assert.Equal(t, 2, requestCount) | ||
|
||
assert.Greater(t, elapsed, 10*time.Millisecond, "Request should have been throttled") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already tested in ratelimit
package. No need to double test things.
t.Run("with nil options uses defaults", func(t *testing.T) { | ||
manager := NewManager(nil) | ||
assert.NotNil(t, manager) | ||
assert.NotNil(t, manager.logger) | ||
assert.Equal(t, 0.02, manager.threshold) | ||
assert.Equal(t, 50*time.Millisecond, manager.minRequestInterval) | ||
assert.Equal(t, 30*time.Second, manager.cleanupInterval) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed. But if you do test the defaults, please use const
-s for the hard coded values. Or at least call the function that creates a default options struct.
func TestDefaultManagerOptions(t *testing.T) { | ||
defaults := DefaultManagerOptions() | ||
|
||
assert.NotNil(t, defaults.Logger) | ||
assert.NotNil(t, defaults.Threshold) | ||
assert.Equal(t, 0.02, *defaults.Threshold) | ||
assert.NotNil(t, defaults.SemaphoreWeight) | ||
assert.Equal(t, int64(50), *defaults.SemaphoreWeight) | ||
assert.NotNil(t, defaults.MinRequestInterval) | ||
assert.Equal(t, 50*time.Millisecond, *defaults.MinRequestInterval) | ||
assert.NotNil(t, defaults.CleanupInterval) | ||
assert.Equal(t, 30*time.Second, *defaults.CleanupInterval) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't we already test the defaults in the above test?
semaphoreWeight := int64(1) | ||
manager := NewManager(&ManagerOptions{ | ||
SemaphoreWeight: &semaphoreWeight, | ||
Logger: logger, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can do stuff like this.
semaphoreWeight := int64(1) | |
manager := NewManager(&ManagerOptions{ | |
SemaphoreWeight: &semaphoreWeight, | |
Logger: logger, | |
}) | |
manager := NewManager(&ManagerOptions{ | |
SemaphoreWeight: toPtr(1), | |
Logger: logger, | |
}) |
Description
What - Add rate limit capabilities to the base port http client
Why - Prevent failing on multiple 429
How - Add resty middle ware that detects rate limits and acts accordingly
Type of change
Please leave one option from the following and delete the rest: